//@ts-nocheck
import readline from 'readline';
import chalk from 'chalk';
import cliCursor from 'cli-cursor';
import cliSpinners from 'cli-spinners';
import logSymbols from 'log-symbols';
import wcwidth from 'wcwidth';
import isInteractive from 'is-interactive';
import isUnicodeSupported from 'is-unicode-supported';
import { BufferListStream } from 'bl';
import { stripAnsi } from './logger';

let stdinDiscarder;

class Ora {
    #linesToClear = 0;

    #isDiscardingStdin = false;

    #lineCount = 0;

    #frameIndex = 0;

    #options;

    #spinner;

    #stream;

    #id;

    #initialInterval;

    #isEnabled;

    #isSilent;

    #indent;

    #text;

    #prefixText;

    color;

    constructor(options) {
        if (!stdinDiscarder) {
            stdinDiscarder = new StdinDiscarder();
        }

        let opts = options;

        if (typeof options === 'string') {
            opts = {
                text: options,
            };
        }

        this.#options = {
            color: 'cyan',
            stream: process.stderr,
            discardStdin: true,
            hideCursor: true,
            ...opts,
        };

        // Public
        this.color = this.#options.color;

        // It's important that these use the public setters.
        this.spinner = this.#options.spinner;

        this.#initialInterval = this.#options.interval;
        this.#stream = this.#options.stream;
        this.#isEnabled =
            typeof this.#options.isEnabled === 'boolean'
                ? this.#options.isEnabled
                : isInteractive({ stream: this.#stream });
        this.#isSilent = typeof this.#options.isSilent === 'boolean' ? this.#options.isSilent : false;

        // Set *after* `this.#stream`.
        // It's important that these use the public setters.
        this.text = this.#options.text;
        this.prefixText = this.#options.prefixText;
        this.indent = this.#options.indent;

        if (process.env.NODE_ENV === 'test') {
            this._stream = this.#stream;
            this._isEnabled = this.#isEnabled;

            Object.defineProperty(this, '_linesToClear', {
                get() {
                    return this.#linesToClear;
                },
                set(newValue) {
                    this.#linesToClear = newValue;
                },
            });

            Object.defineProperty(this, '_frameIndex', {
                get() {
                    return this.#frameIndex;
                },
            });

            Object.defineProperty(this, '_lineCount', {
                get() {
                    return this.#lineCount;
                },
            });
        }
    }

    get indent() {
        return this.#indent;
    }

    set indent(indent = 0) {
        if (!(indent >= 0 && Number.isInteger(indent))) {
            throw new Error('The `indent` option must be an integer from 0 and up');
        }

        this.#indent = indent;
        this.updateLineCount();
    }

    get interval() {
        return this.#initialInterval || this.#spinner.interval || 100;
    }

    get spinner() {
        return this.#spinner;
    }

    set spinner(spinner) {
        this.#frameIndex = 0;
        this.#initialInterval = undefined;

        if (typeof spinner === 'object') {
            if (spinner.frames === undefined) {
                throw new Error('The given spinner must have a `frames` property');
            }

            this.#spinner = spinner;
        } else if (!isUnicodeSupported()) {
            this.#spinner = cliSpinners.line;
        } else if (spinner === undefined) {
            // Set default spinner
            this.#spinner = cliSpinners.dots;
        } else if (spinner !== 'default' && cliSpinners[spinner]) {
            this.#spinner = cliSpinners[spinner];
        } else {
            throw new Error(
                `There is no built-in spinner named '${spinner}'. See https://github.com/sindresorhus/cli-spinners/blob/main/spinners.json for a full list.`
            );
        }
    }

    get text() {
        return this.#text;
    }

    set text(value) {
        this.#text = value || '';
        this.updateLineCount();
    }

    get prefixText() {
        return this.#prefixText;
    }

    set prefixText(value) {
        this.#prefixText = value || '';
        this.updateLineCount();
    }

    get isSpinning() {
        return this.#id !== undefined;
    }

    // TODO: Use private methods when targeting Node.js 14.
    getFullPrefixText(prefixText = this.#prefixText, postfix = ' ') {
        if (typeof prefixText === 'string' && prefixText !== '') {
            return prefixText + postfix;
        }

        if (typeof prefixText === 'function') {
            return prefixText() + postfix;
        }

        return '';
    }

    updateLineCount() {
        const columns = this.#stream.columns || 80;
        const fullPrefixText = this.getFullPrefixText(this.#prefixText, '-');

        this.#lineCount = 0;
        const linesStr = stripAnsi(`${' '.repeat(this.#indent) + fullPrefixText}--${this.#text}`);
        const lines = linesStr.split('\n');
        // for (const line of lines) {
        //     this.#lineCount += Math.max(1, Math.ceil(wcwidth(line) / columns));
        // }
        for (let i = 0; i < lines.length; i++) {
            this.#lineCount += Math.max(1, Math.ceil(wcwidth(lines[i]) / columns));
        }
    }

    get isEnabled() {
        return this.#isEnabled && !this.#isSilent;
    }

    set isEnabled(value) {
        if (typeof value !== 'boolean') {
            throw new TypeError('The `isEnabled` option must be a boolean');
        }

        this.#isEnabled = value;
    }

    get isSilent() {
        return this.#isSilent;
    }

    set isSilent(value) {
        if (typeof value !== 'boolean') {
            throw new TypeError('The `isSilent` option must be a boolean');
        }

        this.#isSilent = value;
    }

    frame() {
        const { frames } = this.#spinner;
        let frame = frames[this.#frameIndex];

        if (this.color) {
            frame = chalk[this.color](frame);
        }

        this.#frameIndex = ++this.#frameIndex % frames.length;
        const fullPrefixText =
            typeof this.#prefixText === 'string' && this.#prefixText !== '' ? `${this.#prefixText} ` : '';
        const fullText = typeof this.text === 'string' ? ` ${this.text}` : '';

        return fullPrefixText + frame + fullText;
    }

    clear() {
        if (!this.#isEnabled || !this.#stream.isTTY) {
            return this;
        }

        this.#stream.cursorTo(0);

        for (let index = 0; index < this.#linesToClear; index++) {
            if (index > 0) {
                this.#stream.moveCursor(0, -1);
            }

            this.#stream.clearLine(1);
        }

        if (this.#indent || this.lastIndent !== this.#indent) {
            this.#stream.cursorTo(this.#indent);
        }

        this.lastIndent = this.#indent;
        this.#linesToClear = 0;

        return this;
    }

    render() {
        if (this.#isSilent) {
            return this;
        }

        this.clear();
        this.#stream.write(this.frame());
        this.#linesToClear = this.#lineCount;

        return this;
    }

    start(text) {
        if (text) {
            this.text = text;
        }

        if (this.#isSilent) {
            return this;
        }

        if (!this.#isEnabled) {
            if (this.text) {
                this.#stream.write(`- ${this.text}\n`);
            }

            return this;
        }

        if (this.isSpinning) {
            return this;
        }

        if (this.#options.hideCursor) {
            cliCursor.hide(this.#stream);
        }

        if (this.#options.discardStdin && process.stdin.isTTY) {
            this.#isDiscardingStdin = true;
            stdinDiscarder.start();
        }

        this.render();
        this.#id = setInterval(this.render.bind(this), this.interval);

        return this;
    }

    stop() {
        if (!this.#isEnabled) {
            return this;
        }

        clearInterval(this.#id);
        this.#id = undefined;
        this.#frameIndex = 0;
        this.clear();
        if (this.#options.hideCursor) {
            cliCursor.show(this.#stream);
        }

        if (this.#options.discardStdin && process.stdin.isTTY && this.#isDiscardingStdin) {
            stdinDiscarder.stop();
            this.#isDiscardingStdin = false;
        }

        return this;
    }

    succeed(text) {
        return this.stopAndPersist({ symbol: logSymbols.success, text });
    }

    fail(text) {
        return this.stopAndPersist({ symbol: logSymbols.error, text });
    }

    warn(text) {
        return this.stopAndPersist({ symbol: logSymbols.warning, text });
    }

    info(text) {
        return this.stopAndPersist({ symbol: logSymbols.info, text });
    }

    stopAndPersist(options = {}) {
        if (this.#isSilent) {
            return this;
        }

        const prefixText = options.prefixText || this.#prefixText;
        const text = options.text || this.text;
        const fullText = typeof text === 'string' ? ` ${text}` : '';

        this.stop();
        this.#stream.write(`${this.getFullPrefixText(prefixText, ' ')}${options.symbol || ' '}${fullText}\n`);

        return this;
    }
}

export default function ora(options) {
    return new Ora(options);
}

export async function oraPromise(action, options) {
    const actionIsFunction = typeof action === 'function';
    const actionIsPromise = typeof action.then === 'function';

    if (!actionIsFunction && !actionIsPromise) {
        throw new TypeError('Parameter `action` must be a Function or a Promise');
    }

    const { successText, failText } =
        typeof options === 'object' ? options : { successText: undefined, failText: undefined };

    const spinner = ora(options).start();

    try {
        const promise = actionIsFunction ? action(spinner) : action;
        const result = await promise;
        const opt1 = typeof successText === 'string' ? successText : successText(result);
        spinner.succeed(successText === undefined ? undefined : opt1);

        return result;
    } catch (error) {
        const opt2 = typeof failText === 'string' ? failText : failText(error);
        spinner.fail(failText === undefined ? undefined : opt2);

        throw error;
    }
}

const ASCII_ETX_CODE = 0x03; // Ctrl+C emits this code

class StdinDiscarder {
    #requests = 0;

    #mutedStream = new BufferListStream();

    #ourEmit;

    #rl;

    constructor() {
        this.#mutedStream.pipe(process.stdout);

        const self = this; //eslint-disable-line
        this.#ourEmit = (event, data, ...args) => {
            const { stdin } = process;
            if (self.#requests > 0 || stdin.emit === self.#ourEmit) {
                if (event === 'keypress') {
                    // Fixes readline behavior
                    return;
                }

                if (event === 'data' && data.includes(ASCII_ETX_CODE)) {
                    process.emit('SIGINT');
                }

                Reflect.apply(self.#ourEmit, this, [event, data, ...args]);
            } else {
                Reflect.apply(process.stdin.emit, this, [event, data, ...args]);
            }
        };
    }

    start() {
        this.#requests++;

        if (this.#requests === 1) {
            this._realStart();
        }
    }

    stop() {
        if (this.#requests <= 0) {
            throw new Error('`stop` called more times than `start`');
        }

        this.#requests--;

        if (this.#requests === 0) {
            this._realStop();
        }
    }

    // TODO: Use private methods when targeting Node.js 14.
    _realStart() {
        // No known way to make it work reliably on Windows
        if (process.platform === 'win32') {
            return;
        }

        this.#rl = readline.createInterface({
            input: process.stdin,
            output: this.#mutedStream,
        });

        this.#rl.on('SIGINT', () => {
            if (process.listenerCount('SIGINT') === 0) {
                process.emit('SIGINT');
            } else {
                this.#rl.close();
                process.kill(process.pid, 'SIGINT');
            }
        });
    }

    _realStop() {
        if (process.platform === 'win32') {
            return;
        }

        this.#rl.close();
        this.#rl = undefined;
    }
}
